
//---------------------------------------------- interfaces --------------------------------------------

// only needed for testing
export interface Timer {
    currentTime(): number;
    setTimeout(fn: () =>void , delay: number): number;
    clearTimeout(token: number): void;
    log(msg: string): void;
}

export interface Scheduler {
    timeout: number;
    slice: number;
   
    isTimeSliceOver(): bool;
    run(fn: () =>Result, name?: string): Task;
}

export interface Continuation {
    getTask(): Task;
    schedule(delay?: number): void;
}

export interface Task {
    name: string;

    interrupt(err: any): void;
    isRunning(): bool;
    join(): void;
    
    getScheduler(): Scheduler;
    whenDone(callback: () =>void ): void;
}

export interface Result {
    then(callback: (any) =>void ): void;
    thenAsync(callback: (any) =>Result): Result;
    isPending(): bool;
}

export interface Frame {
    ident: string;
    raise(err: any): void;
    check(): void;
    finish(result?: any): void;
    getTask(): Task;
    getScheduler(): Scheduler;
    sleep(delay:number): Result;
    timeSlice(): Result;
    loop(func: () =>Result): Result;
    suspend(waitTarget?: any): Continuation;
    result(): Result;
}

export interface EventSource {
    attach(target: EventTarget, type: string): void;
    detach(): void;
    nextEvent(): Result;
}

//---------------------------------------- public entry points ---------------------------------------------------

export function getCurrentTask():Task {
    return currentTask;
}

export function newFrame(ident: string):Frame {
    var frame = new FrameImpl(currentTask, currentTask.top, ident);
    currentTask.top = frame;
    frame.state = F_ACTIVE;
    return frame;
}

export function newEventSource():EventSource {
    var eventSourceImpl = new EventSourceImpl();
    return eventSourceImpl;
}

export function newScheduler(timer?:Timer): Scheduler {
    return new SchedulerImpl(timer || new TimerImpl());
}

//----------------------------------------- implementations ------------------------------------------------

var currentTask: TaskImpl;
var taskCount = 0;

interface Comparable {
    compare(other: Comparable):number;
}

/*
// lame but safe implementation
class PriorityQueue {
    queue: Comparable[] = [];

    length(): number {
        return this.queue.length;
    }

    add(item: Comparable): void {
        this.queue.push(item);
        this.sort();
    }

    bumpUp(item: Comparable): void {
        this.sort();
    }

    remove(): Comparable {
        return this.queue.pop();
    }

    peek(): Comparable {
        return this.queue[this.queue.length - 1];
    }

    private sort(): void {
        this.queue.sort((a: Comparable, b: Comparable) => -b.compare(a));
    }
}
*/

class PriorityQueue {
    queue: Comparable[] = [null];
    
    constructor () {
    }

    length():number {
        return this.queue.length - 1;
    }

    add(item: Comparable): void {
        var index = this.queue.length;
        while (index > 1) {
            var parentIndex = Math.floor(index / 2);
            var parent = this.queue[parentIndex];
            if (parent.compare(item) > 0) {
                this.queue[index] = item;
                return;
            }
            this.queue[index] = parent;
            index = parentIndex;
        }
        this.queue[1] = item;
    }

    peek(): Comparable {
        return this.queue[1];
    }

    remove():Comparable {
        var result = this.queue[1];
        var curr = this.queue.pop();
        var size = this.queue.length;
        if( size > 0 ) {
            var index = 1;
            while( true ) {
	            var childIndex = index*2;
	            if( childIndex >= size )
	                break;
	            if( this.queue[childIndex].compare(curr) > 0 ) {
	                if( childIndex+1 < size && 
                        this.queue[childIndex+1].compare(this.queue[childIndex]) > 0 ) {
	                    childIndex++;
	                }
	            } else if( childIndex+1 < size && this.queue[childIndex+1].compare(curr) > 0 ) {
	              childIndex++;
	            } else {
	              break;
	            }
	            this.queue[index] = this.queue[childIndex];
	            index = childIndex;
           }
           this.queue[index] = curr;
        }
        return result;
    }
}


class TimerImpl implements Timer {
    currentTime(): number {
        return (new Date()).valueOf();
    }

    setTimeout(fn: () =>void , delay: number): number {
        return setTimeout(fn, delay);
    }

    clearTimeout(token: number): void {
        clearTimeout(token);
    }

    log(msg: string): void {
        if( window.console && window.console.log )
            window.console.log(msg);
    }
}

class SchedulerImpl implements Scheduler {
    timeout = 1;
    slice = 25;
    sliceOverTime = 0;
    taskCount = 0;
    queue: PriorityQueue = new PriorityQueue();
    wakeupTime: number;
    timeoutToken: number = null;
    inTimeSlice = false;
    order = 0;

    constructor (public timer: Timer) {
    }

    isTimeSliceOver():bool {
        var now = this.timer.currentTime();
        return now >= this.sliceOverTime;
    }

    private arm(): void {
        if( this.inTimeSlice )
            return;
        var nextInQueue = <ContinuationImpl>this.queue.peek();
        var newTime = nextInQueue.scheduledTime;
        var now = this.timer.currentTime();
        if (this.timeoutToken != null) {
            if( now + this.timeout > this.wakeupTime )
                return; // no use re-arming
            this.timer.clearTimeout(this.timeoutToken);
        }
        var timeout = newTime - now;
        if( timeout <= this.timeout )
            timeout = this.timeout;
        this.wakeupTime = now + timeout;
        this.timeoutToken = this.timer.setTimeout(() =>{
            this.timeoutToken = null;
            this.doTimeSlice();
        }, timeout);
    }

    schedule(continuation: Continuation, delay?:number): void {
        var c = <ContinuationImpl>continuation;
        var now = this.timer.currentTime();
        c.order = this.order++;
        c.scheduledTime = now + (delay || 0);
        this.queue.add(c);
        this.arm();
    }

    doTimeSlice(): void {
        if (this.timeoutToken != null) {
            this.timer.clearTimeout(this.timeoutToken)
            this.timeoutToken = null;
        }
        this.inTimeSlice = true;
        try {
            var now = this.timer.currentTime();
            this.sliceOverTime = now + this.slice;
            while (this.queue.length()) {
                var continuation = <ContinuationImpl>this.queue.peek();
                if( continuation.scheduledTime > now )
                    break; // too early
                this.queue.remove();
                continuation.resumeInternal();
                now = this.timer.currentTime();
                if( now >= this.sliceOverTime )
                    break;
            }
        } catch (err) {
        }
        this.inTimeSlice = false;
        if( this.queue.length() )
            this.arm();
    }

    run(func: ()=>Result, name?:string ):Task {
        var task = new TaskImpl(this, name||"task" + (this.taskCount++));
        task.top = new FrameImpl(task, null, "bootstrap");
        task.top.state = F_ACTIVE;
        task.top.then(() =>{
            var done = () =>{
                task.running = false;
                if (task.exception) {
                    this.timer.log("Unhandled exception in task " + task.name + ": " + task.exception);
                    if (task.exception["frameTrace"])
                        this.timer.log(task.exception["frameTrace"]);
                }
                for (var i = 0 ; i < task.callbacks.length ; i++) {
                    var callback = task.callbacks[i];
                    try {
                        callback();
                    } catch (err) {
                    }
                }
            }
            try {
                func().then(function (result) {
                    task.result = result;
                    done();
                });
            } catch (err) {
                task.raise(err);
                done();
            }
        });
        var savedTask = currentTask;
        currentTask = task;
        this.schedule(task.top.suspend("bootstrap"));
        currentTask = savedTask;
        return task;
    }
}

class ContinuationImpl implements Continuation, Comparable {
    scheduledTime: number = 0;
    order: number = 0;

    constructor (public task: TaskImpl) {
    }

    compare(other: ContinuationImpl): number {
        // earlier wins
        return other.scheduledTime - this.scheduledTime || other.order - this.order;
    }

    getTask(): Task {
        return this.task;
    }

    schedule(delay?: number): void {
        this.task.scheduler.schedule(this, delay);
    }

    resumeInternal():bool {
        var task = this.task;
        this.task = null;
        if (task && task.continuation == this) {
            task.continuation = null;
            var savedTask = currentTask;
            currentTask = task;
            currentTask.top.finish();
            currentTask = savedTask;
            return true;
        }
        return false;
    }
}

class TaskImpl implements Task {

    callbacks: { (): void; }[] = [];
    exception: any;
    running: bool = true;
    result: any;
    waitTarget: any;
    top: FrameImpl;
    continuation:Continuation;

    constructor (public scheduler:SchedulerImpl, public name: string) {
    }

    interrupt(err: any): void {
        this.raise( err || new Error("E_TASK_INTERRUPT") );
        if (this !== currentTask && this.continuation) {
            // blocked on something
            var continuation = new ContinuationImpl(this);
            this.waitTarget = "interrupt";
            this.continuation = continuation;
            this.scheduler.schedule(continuation);
        }
    }

    getScheduler():Scheduler {
        return this.scheduler;
    }

    isRunning(): bool {
        return this.running;
    }

    whenDone(callback: () =>void ): void {
        this.callbacks.push(callback);
    }

    join(): Frame {
        var frame = newFrame("Task.join");
        if (!this.running) {
            frame.finish();
            return frame;
        }
        var continuation = frame.suspend(this);
        this.whenDone(function () {
            continuation.schedule();
        });
        return frame;
    }

    raise(err: any): void {
        if( typeof err != "object" )
            err = new Error(err + "");
        if (!err["frameTrace"]) {
            var out = "";
            for (var f = this.top ; f ; f = f.parent) {
                out += "\t";
                out += f.ident;
                out += "\n";
            }
            err["frameTrace"] = out;
        }
        this.exception = err;
    }
}

var F_INIT = 0; // before newFrame call
var F_ACTIVE = 1; // before finish call
var F_FINISHED = 2; // before callback complete
var F_DEAD = 3; // when callback is complete

class ResultImpl implements Result {
    constructor (public frame: FrameImpl) {
    }

    then(callback: (any) =>void ):void {
        this.frame.then(callback);
    }

    thenAsync(callback: (any) =>Result ):Result {
        if (this.isPending()) {
            // thenAsync is special, do the trick with the context 
            var frame = new FrameImpl(this.frame.task, this.frame.parent, "AsyncResult.thenAsync");
            frame.state = F_ACTIVE;
            this.frame.parent = frame;
            this.frame.then((res1: any) =>callback(res1).then((res2: any) =>frame.finish(res2)));
            return frame.result();
        } else {
            return callback(this.frame.res);
        }
    }

    isPending():bool {
        return this.frame.state == F_ACTIVE;
    }
}

class FrameImpl implements Frame {

    res: any;
    state = F_INIT;
    callback: (any) =>void;

    constructor (public task: TaskImpl, public parent: FrameImpl, public ident: string) {
    }

    checkEnvironment(): bool {
        if (!currentTask)
            throw new Error("F_TASK_NO_CONTEXT");
        if (this != currentTask.top) {
            currentTask.raise("F_TASK_NOT_TOP_FRAME");
            return false;
        }
        return true;
    }

    raise(err: any): void {
        if (!this.checkEnvironment())
            return;
        this.task.raise(err);
        this.finish();
    }

    check(): void {
        if (!this.checkEnvironment())
            return;
        var err = this.task.exception;
        if (err) {
            this.task.exception = null;
            throw err;
        }
    }

    result(): Result {
        return new ResultImpl(this);
    }

    finish(res?: any): void {
        if (!this.checkEnvironment())
            return;

        if( !currentTask.exception )
            this.res = res;
        this.state = F_FINISHED;

        do {
            var top = currentTask.top;
            currentTask.top = top.parent;
            if (!top.callback) {
                // let it attach the callback
                break;
            }
            try {
                top.callback(res);
            } catch (err) {
                this.task.raise(err);
            }
            top.state = F_DEAD;
        } while (currentTask && currentTask.top && currentTask.exception);
    }

    getTask(): Task {
        return this.task;
    }

    getScheduler(): Scheduler {
        return this.task.scheduler;
    }

    then(callback: (any) =>void ): void {
        // legal to call when currentTask is null
        switch (this.state) {
            case F_ACTIVE:
                if (this.callback) {
                    throw new Error("F_TASK_FRAME_ALREADY_HAS_CALLBACK");
                } else {
                    this.callback = callback;
                }
                break;
            case F_FINISHED:
                try {
                    callback(this.res);
                    this.state = F_DEAD;
                } catch (err) {
                    this.state = F_DEAD;
                    currentTask.raise(err);
                    if (currentTask.top)
                        currentTask.top.finish();
                }
                break;
            case F_DEAD:
                throw new Error("F_TASK_DEAD_FRAME");
            default:
                throw new Error("F_TASK_UNEXPECTED_FRAME_STATE");
        }
    }

    timeSlice(): Result {
        var frame = newFrame("Frame.timeSlice");
        var scheduler = frame.getScheduler();
        if( scheduler.isTimeSliceOver() )
            frame.suspend().schedule();
        else
            frame.finish();
        return frame.result();
    }

    sleep(delay: number): Result {
        var frame = newFrame("Frame.sleep");
        var scheduler = frame.getScheduler();
        frame.suspend().schedule(delay);
        return frame.result();
    }

    loop(func: () =>Result):Result {
        var frame = newFrame("Frame.loop");
        var step = function () {
            var result: Result;
            try {
                frame.check();
                result = func();
                if (result)
                    result.then(step);
                else
                    frame.finish();
            } catch (err) {
                frame.raise(err);
            }
        };
        step();
        return frame.result();
    }

    suspend(waitTarget?: any):Continuation {
        this.checkEnvironment();
        if( this.task.continuation )
            throw new Error("E_TASK_ALREADY_SUSPENDED");
        var continuation = new ContinuationImpl(this.task);
        this.task.continuation = continuation;
        currentTask = null;
        this.task.waitTarget = waitTarget;
        return continuation;
    }
}

interface EventItem {
    event: Event;
    next: EventItem;
}

class EventSourceImpl implements EventSource {

    target: EventTarget;
    type: string;
    head: EventItem;
    tail: EventItem;
    continuation: Continuation;
    listener: (event: Event) =>void;

    constructor () {
        this.head = { event: null, next: null };
        this.tail = this.head;
    }

    attach(target: EventTarget, type: string): void {
        if( this.target )
            throw new Error("E_TASK_EVENT_SOURCE_ALREADY_ATTACHED");
        this.target = target;
        this.type = type;
        this.listener = (event: Event):void =>{
            if (this.tail.event) {
                this.tail.next = { event: event, next: null };
                this.tail = this.tail.next;
            } else {
                this.tail.event = event;
                var continuation = this.continuation;
                if (continuation) {
                    this.continuation = null;
                    continuation.schedule();
                }
            }
        };
        target.addEventListener(type, this.listener, false);
    }

    detach():void {
        if( !this.target )
            throw new Error("E_TASK_EVENT_SOURCE_NOT_ATTACHED");
        this.target.removeEventListener(this.type, this.listener, false);
        this.target = null;
        this.type = null;
        this.listener = null;
    }

    nextEvent(): Result {
        var frame = newFrame("EventSource.nextEvent");
        var readEvent = ()=>{
            if (this.head.event) {
                var event = this.head.event;
                if (this.head.next)
                    this.head = this.head.next;
                else
                    this.head.event = null;
                frame.finish(event);
            } else if (this.continuation) {
                throw new Error("E_TASK_EVENT_SOURCE_OTHER_TASK_WAITING");
            } else {
                var frameInternal = newFrame("EventSource.nextEventInternal");
                this.continuation = frameInternal.suspend(this);
                frameInternal.result().then(readEvent);
            }
        };
        readEvent();
        return frame.result();
    }
}